﻿using System;
using System.Collections.Generic;
using System.Globalization;

namespace Nomo.CoreModule
{
    /// <summary>
    /// 中文数字格式化器。
    /// </summary>
    public static class ChineseNumberFormatter
    {
        private const int MaxUnitaryIntegralPartDigits = 16;

        private const char Zero     = '0';
        private const char One      = '1';
        private const char Two      = '2';
        private const char Three    = '3';
        private const char Four     = '4';
        private const char Five     = '5';
        private const char Six      = '6';
        private const char Seven    = '7';
        private const char Eight    = '8';
        private const char Nine     = '9';
        private const char Dot      = '.';
        private const char Negative = '-';

        private const char Ling = '零';
        private const char Yi   = '一';
        private const char Er   = '二';
        private const char San  = '三';
        private const char Si   = '四';
        private const char Wu   = '五';
        private const char Liu  = '六';
        private const char Qi   = '七';
        private const char Ba   = '八';
        private const char Jiu  = '九';
        private const char Dian = '点';
        private const char Fu   = '负';

        private const string PositiveInfinity = "正无穷大";
        private const string NegativeInfinity = "负无穷大";
        private const string NaN              = "不是数字";

        private static readonly IFormatProvider FormatProvider = new NumberFormatInfo
        {
            NumberGroupSeparator = string.Empty,
            NumberGroupSizes     = new int[0]
        };

        private static readonly char[] UnitsInNumberGroup =
        {
            '十', // 10
            '百', // 100
            '千'  // 1000
        };

        private static readonly char[] UnitsOfGroups =
        {
            '万', // 1_0000
            '亿', // 1_0000_0000
            '兆'  // 1_0000_0000_0000
        };

        public static string ToString(int value, bool unitary = true)
        {
            var str   = value.ToString("N", FormatProvider);
            var chars = str.ToCharArray();
            DoFormat(ref chars, unitary);
            return new string(chars);
        }

        public static string ToString(uint value, bool unitary = true)
        {
            var str   = value.ToString("N", FormatProvider);
            var chars = str.ToCharArray();
            DoFormat(ref chars, unitary);
            return new string(chars);
        }

        public static string ToString(long value, bool unitary = true)
        {
            var str   = value.ToString("N", FormatProvider);
            var chars = str.ToCharArray();
            DoFormat(ref chars, unitary);
            return new string(chars);
        }

        public static string ToString(ulong value, bool unitary = true)
        {
            var str   = value.ToString("N", FormatProvider);
            var chars = str.ToCharArray();
            DoFormat(ref chars, unitary);
            return new string(chars);
        }

        public static string ToString(double value, bool unitary = true)
        {
            if (double.IsPositiveInfinity(value))
            {
                return PositiveInfinity;
            }
            if (double.IsNegativeInfinity(value))
            {
                return NegativeInfinity;
            }
            if (double.IsNaN(value))
            {
                return NaN;
            }

            var str   = value.ToString("G17", FormatProvider);
            var chars = str.ToCharArray();
            DoFormat(ref chars, unitary);
            return new string(chars);
        }

        public static string ToString(float value, bool unitary = true)
        {
            if (float.IsPositiveInfinity(value))
            {
                return PositiveInfinity;
            }
            if (float.IsNegativeInfinity(value))
            {
                return NegativeInfinity;
            }
            if (float.IsNaN(value))
            {
                return NaN;
            }

            var str   = value.ToString("G9", FormatProvider);
            var chars = str.ToCharArray();
            DoFormat(ref chars, unitary);
            return new string(chars);
        }

        public static string ToString(decimal value, bool unitary = true)
        {
            var str   = value.ToString("N29", FormatProvider);
            var chars = str.ToCharArray();
            DoFormat(ref chars, unitary);
            return new string(chars);
        }

        private static void DoFormat(ref char[] chars, bool unitary = true)
        {
            if (chars == null)
            {
                throw new ArgumentNullException(nameof(chars));
            }

            var length = chars.Length;

            if (length == 0)
            {
                throw new ArgumentException();
            }

            var negative = chars[0] == Negative;
            if (negative)
            {
                var array = new char[chars.Length - 1];
                Array.Copy(chars, 1, array, 0, chars.Length - 1);
                chars  = array;
                length = chars.Length;
            }

            var hasDot   = false;
            var dotIndex = 0;

            for (var i = 0; i < length; i++)
            {
                var c = chars[i];
                switch (c)
                {
                    case Zero:
                    case One:
                    case Two:
                    case Three:
                    case Four:
                    case Five:
                    case Six:
                    case Seven:
                    case Eight:
                    case Nine:
                        break;
                    case Dot:
                        if (!hasDot)
                        {
                            hasDot   = true;
                            dotIndex = i;
                        }
                        else
                        {
                            throw new ArgumentException($"含有多个 {Dot} 字符。");
                        }
                        break;
                }
            }

            var integralPart = new char[dotIndex];
            Array.Copy(chars, 0, integralPart, 0, dotIndex);
            if (hasDot)
            {
                if (unitary)
                {
                    if (integralPart.Length > MaxUnitaryIntegralPartDigits)
                    {
                        throw new NotSupportedException($"整数部分的字符数超过了 {MaxUnitaryIntegralPartDigits} 位。");
                    }
                    FormatUnitary(ref integralPart);
                }
                else
                {
                    FormatSimply(ref integralPart);
                }
                var fractionalPart = new char[length - 1 - dotIndex];
                Array.Copy(chars, dotIndex + 1, fractionalPart, 0, length - 1 - dotIndex);
                FormatSimply(ref fractionalPart);

                var fractionalPartZeroCountFromEnd = default(int);
                for (var i = fractionalPart.Length - 1; i >= 0; i--)
                {
                    if (fractionalPart[i] == Ling)
                    {
                        fractionalPartZeroCountFromEnd++;
                    }
                    else
                    {
                        break;
                    }
                }
                if (fractionalPartZeroCountFromEnd != default)
                {
                    if (fractionalPartZeroCountFromEnd == fractionalPart.Length)
                    {
                        fractionalPart = new char[0];
                        hasDot         = false;
                    }
                    else
                    {
                        var newFractionalPart = new char[fractionalPart.Length - fractionalPartZeroCountFromEnd];
                        Array.Copy(fractionalPart, 0, newFractionalPart, 0, fractionalPart.Length - fractionalPartZeroCountFromEnd);
                        fractionalPart = newFractionalPart;
                    }
                }

                var integralPartLength   = integralPart.Length;
                var fractionalPartLength = fractionalPart.Length;
                chars = new char[integralPartLength + 1 + fractionalPartLength];
                Array.Copy(integralPart, 0, chars, 0, integralPartLength);
                if (hasDot)
                {
                    chars[integralPartLength] = Dian;
                }
                Array.Copy(fractionalPart, 0, chars, integralPartLength + 1, fractionalPartLength);
            }
            else
            {
                if (unitary)
                {
                    FormatUnitary(ref integralPart);
                }
                else
                {
                    FormatSimply(ref integralPart);
                }
                chars = integralPart;
            }

            // 将开头处的“一十”替换为“十”
            if (chars.Length >= 2 && chars[0] == Yi && chars[1] == UnitsInNumberGroup[0])
            {
                var array = new char[chars.Length - 1];
                Array.Copy(chars, 1, array, 0, chars.Length - 1);
                chars = array;
            }

            if (negative)
            {
                var array = new char[chars.Length + 1];
                array[0] = Fu;
                Array.Copy(chars, 0, array, 1, chars.Length);
                chars = array;
            }
        }

        private static void FormatUnitary(ref char[] chars)
        {
            // 从右侧开始，将 chars 每隔 4 个字符分成一组，允许最左侧的组的字符数不满 4 个
            // [1, 2, 3, 4, 5, 6, 7, 8, 9] => [[1], [2, 3, 4, 5], [6, 7, 8, 9]]
            var    length       = chars.Length;
            var    groupCount   = (length + 3) / 4;
            var    numberGroups = new char[groupCount][];
            char[] group;
            int    groupLength;
            for (var i = 1; i < groupCount; i++)
            {
                groupLength = 4;
                group       = new char[groupLength];
                Array.Copy(chars, length - (groupCount - i) * 4, group, 0, 4);
                numberGroups[i] = group;
            }
            groupLength = length - (groupCount - 1) * 4;
            group       = new char[length - (groupCount - 1) * 4];
            Array.Copy(chars, 0, group, 0, groupLength);
            numberGroups[0] = group;

            for (var i = 0; i < numberGroups.Length; i++)
            {
                var numberGroup = numberGroups[i];
                var zeroCount   = default(int);
                for (var j = 0; j < numberGroup.Length; j++)
                {
                    if (numberGroup[j] == Zero)
                    {
                        zeroCount++;
                    }
                    else
                    {
                        break;
                    }
                }
                if (zeroCount != default)
                {
                    var array = new char[numberGroup.Length - zeroCount];
                    Array.Copy(numberGroup, zeroCount, array, 0, numberGroup.Length - zeroCount);
                    numberGroups[i] = array;
                }
            }

            for (var i = 0; i < numberGroups.Length; i++)
            {
                if (numberGroups[i].Length == 0)
                {
                    continue;
                }
                AddUnitInNumberGroup(ref numberGroups[i]);
            }

            // 给组加上单位
            var stack = new Stack<char>();
            for (var i = numberGroups[groupCount - 1].Length - 1; i >= 0; i--)
            {
                stack.Push(numberGroups[groupCount - 1][i]);
            }
            for (int i = groupCount - 2, k = 0; i >= 0; i--, k++)
            {
                var numberGroup = numberGroups[i];
                if (numberGroup.Length == 0)
                {
                    if (stack.Count != 0 && stack.Peek() != Ling)
                    {
                        stack.Push(Ling);
                    }
                    continue;
                }
                stack.Push(UnitsOfGroups[k]);
                for (var j = numberGroup.Length - 1; j >= 0; j--)
                {
                    stack.Push(numberGroup[j]);
                }
            }

            if (stack.Count != 1 && stack.Peek() == Ling)
            {
                stack.Pop();
            }

            chars = stack.ToArray();
        }

        private static void AddUnitInNumberGroup(ref char[] group)
        {
            FormatSimply(ref group);
            var stack       = new Stack<char>();
            var length      = group.Length;
            var lastElement = group[length - 1];
            if (lastElement != Ling)
            {
                stack.Push(lastElement);
            }

            for (int i = length - 2, k = 0; i >= 0; i--, k++)
            {
                var c = group[i];
                if (c == Ling)
                {
                    if (stack.Count != 0 && stack.Peek() != Ling)
                    {
                        stack.Push(Ling);
                    }
                    continue;
                }
                stack.Push(UnitsInNumberGroup[k]);
                stack.Push(c);
            }

            if (group.Length != 4)
            {
                stack.Push(Ling);
            }

            group = stack.ToArray();
        }

        private static void FormatSimply(ref char[] chars)
        {
            for (var i = 0; i < chars.Length; i++)
            {
                switch (chars[i])
                {
                    case Zero:
                        chars[i] = Ling;
                        break;
                    case One:
                        chars[i] = Yi;
                        break;
                    case Two:
                        chars[i] = Er;
                        break;
                    case Three:
                        chars[i] = San;
                        break;
                    case Four:
                        chars[i] = Si;
                        break;
                    case Five:
                        chars[i] = Wu;
                        break;
                    case Six:
                        chars[i] = Liu;
                        break;
                    case Seven:
                        chars[i] = Qi;
                        break;
                    case Eight:
                        chars[i] = Ba;
                        break;
                    case Nine:
                        chars[i] = Jiu;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
    }
}
